package com.hzb.myvpn.model;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;

import android.util.Log;

import com.hzb.myvpn.model.ITunDev.ITunDevFactory;

public abstract class BaseVpn {
    public static class VpnConnectException extends Exception {

        /**
         * 
         */
        private static final long serialVersionUID = 4245918787838424551L;
        
        public VpnConnectException(Throwable e) {
            super(e);
        }
    }
    protected abstract ConnInfo connect() throws Exception;

    protected abstract void encode(byte[] src, short size, ByteBuffer dst)
            throws Exception;

    protected abstract short decode(ByteBuffer src, byte[] dst) throws Exception;

    protected VpnConfig config;
    private ITunDevFactory tunFactory;
    protected ITunDev tunDev;
    protected ConnInfo connInfo;
    protected boolean running = true;
    protected Selector selector;

    public BaseVpn(VpnConfig config, ITunDevFactory tunFactory) throws IOException {
        this.config = config;
        this.tunFactory = tunFactory;
        this.selector = Selector.open();
    }

    public void tunneling() throws Exception {
        try {
            this.connInfo = connect();
        } catch (Throwable e) {
            Log.e("Vpn", "Error connecting to vpn server");
            throw new VpnConnectException(e);
        }
        if (connInfo.getRoutes() == null) connInfo.setRoutes(new ArrayList<Route>());
        if (config.getRoutes() != null) connInfo.getRoutes().addAll(config.getRoutes());
        this.tunDev = tunFactory.createTunDev(connInfo);
        Log.i("Vpn", "Myvpn connected");
        SocketChannel sc = connInfo.getSocket();
        sc.configureBlocking(false);
        
        SelectionKey key = sc.register(selector, SelectionKey.OP_READ);
        // Normally in write state
        ByteBuffer readBuffer = ByteBuffer.allocate(config.getReadBufferSize());
        readBuffer.order(ByteOrder.BIG_ENDIAN);
        final ByteBuffer writeBuffer = ByteBuffer.allocate(config.getWriteBufferSize());
        writeBuffer.order(ByteOrder.BIG_ENDIAN);
        
        Thread tunThread = new Thread("TunReadThread") {
            @Override
            public void run() {
                try {
                    for (;running;) {
                        byte[] tunBuffer = new byte[2000];
                        short nread = tunDev.read(tunBuffer, 5);
                        if (nread < 0) {
                            running = false;
                            break;
                        }
                        if (nread > 0 && running) {
                            synchronized (writeBuffer) {
                                encode(tunBuffer, nread, writeBuffer);
                                selector.wakeup();
                            }
                        }
                    }
                } catch (Exception e) {
                    Log.e("Tun", "Error reading tun", e);
                    running = false;
                }
            }
        };
        tunThread.start();
        
        try {
            long lastSelect0 = 0;
            int select0count = 0;
            long lastReSelect = 0;
            boolean writing = false;
            byte[] tunBuffer = new byte[2000];
            long lastReadFromNet = System.currentTimeMillis();
            long lastWriteToNet = lastReadFromNet;
            long selectInterval = 15000;
            long heartbeatInterval = config.getHeartbeat() * 1000;
            long heartbeatTimeout = config.getTimeout() * 1000;
            SelectLoop:
            for (;running;) {
                if (writeBuffer.position() > 0) {
                    if (!writing) {
                        writing = true;
                        key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                    }
                }
                int count = selector.select(selectInterval);
                long now = System.currentTimeMillis();
                if (count == 0) {
                    if (now - lastSelect0 < 100 && select0count > 10) {
                        Log.e("Vpn", "Select busy loop");
                        try {
                            synchronized (writeBuffer) {
                                if (now - lastReSelect < 1000) throw new IOException("Fail solving selector busy loop");
                                key.cancel();
                                selector.close();
                                selector = Selector.open();
                                key = sc.register(selector, SelectionKey.OP_READ);
                                writing = false;
                                lastSelect0 = 0;
                                select0count = 0;
                                lastReSelect = now;
                            }
                        } catch (Throwable e) {
                            Log.e("Vpn", "Error try solving selector busy loop", e);
                            running = false;
                        }
                    }
                    lastSelect0 = now;
                    select0count ++;
                    continue;
                }
                lastSelect0 = 0;
                select0count = 0;
                
                selector.selectedKeys().clear();
                
                if (key.isWritable()) {
                    synchronized (writeBuffer) {
                        writeBuffer.flip();
                        sc.write(writeBuffer);
                        if (!writeBuffer.hasRemaining()) {
                            writing = false;
                            key.interestOps(SelectionKey.OP_READ);
                        }
                        writeBuffer.compact();
                        lastWriteToNet = now;
                    }
                }
                if (key.isReadable()) {
                    lastReadFromNet = now;
                    int nread = sc.read(readBuffer);
                    if (nread < 0) {
                        Log.i("Vpn", "Reconnecting vpn");
                        for (int i=0; i<3; i++) {
                            try {
                                ConnInfo tmp = connect();
                                if (tmp.getLocalIp().equals(connInfo.getLocalIp()) && tmp.getRemoteIp().equals(connInfo.getRemoteIp())) {
                                    try {
                                        key.cancel();
                                    } catch (Exception e) {
                                        Log.e("Vpn", "Error canceling SelectionKey", e);
                                    }
                                    try {
                                        sc.close();
                                    } catch (Exception e) {
                                        Log.e("Vpn", "Error closing SocketChannel", e);
                                    }
                                    synchronized (writeBuffer) {
                                        this.connInfo = tmp;
                                        sc = tmp.getSocket();
                                        sc.configureBlocking(false);
                                        key = sc.register(selector, SelectionKey.OP_READ);
                                        writing = false;
                                        lastReadFromNet = System.currentTimeMillis();
                                        lastSelect0 = 0;
                                        select0count = 0;
                                        readBuffer.clear();
                                        writeBuffer.clear();
                                        continue SelectLoop;
                                    }
                                } else {
                                    break;
                                }
                            } catch (Throwable e) {
                                Log.e("Vpn", "Error reconnecting vpn", e);
                                Thread.sleep(3000);
                            }
                        }
                        throw new IOException("SocketChannel read return -1");
                    }
                    if (nread > 0 && readBuffer.position() > 0) {
                        readBuffer.flip();
                        for (;readBuffer.hasRemaining();) {
                            int dataSize = decode(readBuffer, tunBuffer);
                            if (dataSize < 0) break;
                            else if (dataSize > 0) tunDev.write(tunBuffer, dataSize);
                            else Log.i("Vpn", "Receive heartbeat");
                        }
                        readBuffer.compact();
                    }
                } else if (now - lastReadFromNet > heartbeatTimeout) {
                    throw new IOException("Heartbeat timeout");
                }
                
                if (now - lastWriteToNet > heartbeatInterval) {
                    synchronized (writeBuffer) {
                        lastWriteToNet = now;
                        encode(tunBuffer, (short)0, writeBuffer);
                        Log.i("Vpn", "Trigger heartbeat");
                    }
                }
            }
        } finally {
            running = false;
            try {
                key.cancel();
            } catch (Exception e) {
                Log.e("Vpn", "Error canceling SelectionKey", e);
            }
            try {
                sc.close();
            } catch (Exception e) {
                Log.e("Vpn", "Error closing SocketChannel", e);
            }
            try {
                selector.close();
            } catch (Exception e) {
                Log.e("Vpn", "Error closing Selector", e);
            }
            try {
                tunDev.close();
            } catch (Exception e) {
                Log.e("Vpn", "Error closing TunDev", e);
            }
        }
    }

    public void close() {
        running = false;
        try {
            selector.wakeup();
        } catch (Exception e) {
            Log.e("Vpn", "Selector is closed", e);
        }
    }
}
